package server;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import server.configuration.ServerConfiguration;
import server.configuration.ServerConfigurationManager;
import server.repositories.AccountRepository;
import server.threads.ConnectionChecker;
import server.threads.ServerCommunicator;

import message.AddServerRequestMessage;
import message.BulkUpdateRequestMessage;

public class Server
{
	private boolean running;
	private ServerCommunicator communicator;
	private ConnectionChecker connectionChecker;
	private ConnectionMap connections;
	private ServerSocket serverSocket;
	private boolean ready;
	private final ServerConfiguration config;
	private final ServerConfigurationManager configManager;
	private final AccountRepository accountRepository;
	private boolean bulkUpdateCompleted;
	
	static
	{
		boolean driverLoaded = false;
		
		while (!driverLoaded)
		{
			try
			{
				Class.forName("com.mysql.jdbc.Driver");
				driverLoaded = true;
			}
			catch (ClassNotFoundException e) {}
		}
	}
	public Server() throws InstantiationException
	{
		this("config2.xml");
	}
	public Server(String configFileLocation) throws InstantiationException
	{
		this.configManager = new ServerConfigurationManager(configFileLocation);
		this.config = configManager.loadConfiguration();
		this.accountRepository = new AccountRepository(config.getDatabaseLocation(), config.getDatabaseUsername(), config.getDatabasePassword());
	}
	public AccountRepository getAccountRepository()
	{
		return accountRepository;
	}
	public void addServerConnection(int connectionId, String serverLocation)
	{
		if (config.getKnownServers() != null &&!config.getKnownServers().contains(serverLocation))
		{
			config.addKnownServer(serverLocation);
			
			this.saveConfig();
		}
		
		Connection connection = connections.get(connectionId);
		
		if (connection != null)
			connections.putServerConnection(connection.connectionId, connection);
	}
	public int getConnectionCount()
	{
		return communicator.getConnectionCount();
	}
	public java.sql.Connection getDatabaseConnection()
	{
		try
		{
			return DriverManager.getConnection(config.getDatabaseLocation(), config.getDatabaseUsername(), config.getDatabasePassword());
		}
		catch (SQLException e)
		{
			return null;
		}
	}
	public int getConnectionId()
	{
		return communicator.getAvailableConnectionId();
	}
	public void stop()
	{
		Connection[] connections = this.connections.values().toArray(new Connection[0]);
		
		for (Connection connection : connections)
			try
			{
				connection.close();
			}
			catch (IOException e) {}
		
		communicator.stopRunning();
		connectionChecker.stopRunning();
		
		try
		{
			this.serverSocket.close();
		}
		catch (IOException e) {}
		this.running = false;
	}
	public void dataSynchronized()
	{
		if (this.isReady() || bulkUpdateCompleted)
		{
			config.setLastSynchronized(System.currentTimeMillis() / 1000);
			
			this.saveConfig();
		}
	}
	public void bulkUpdateCompleted()
	{
		bulkUpdateCompleted = true;
		
		this.dataSynchronized();
	}
	private void sendBulkUpdateMessage()
	{
		BulkUpdateRequestMessage message = new BulkUpdateRequestMessage();
		
		message.setLastSynchronizeTimestamp(config.getLastSynchronized());
		
		if (connections.serverConnectionValues().size() > 0)
			connections.serverConnectionValues().toArray(new Connection[0])[0].sendMessage(message);
	}
	private void synchronizeData()
	{
		final int waitTime = 100;
		final int waitTicks = 600;
		int tickCount = 0;
		
		if (this.connections.serverConnectionValues().size() > 0)
		{
			sendBulkUpdateMessage();
			
			while (!bulkUpdateCompleted)
			{
				try
				{
					if (tickCount == waitTicks)
					{
						tickCount = 0;
						sendBulkUpdateMessage();
					}
					
					Thread.sleep(waitTime);
				}
				catch (InterruptedException e) {}
			}
		}
	}
	private void start()
	{
		final int connectionWaitTime = 100;
		
		ready = false;
		running = true;
		connections = new ConnectionMap();
		communicator = new ServerCommunicator(this, connections);
		connectionChecker = new ConnectionChecker(this, connections);
		bulkUpdateCompleted = false;
		
		communicator.start();
		connectionChecker.start();
		
		this.connectToServers();
		
		System.out.print("Synchronizing... ");
		
		this.synchronizeData();
		
		System.out.println("Done!");
		
		try
		{
			serverSocket = new ServerSocket(config.getPort());
			
			serverSocket.setSoTimeout(connectionWaitTime);
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		
		ready = true;
	}
	public boolean isRunning()
	{
		return running;
	}
	private void connectToServers()
	{
		if (config.getKnownServers() != null)
		{
			for (String serverAddress : config.getKnownServers())
			{
				Pattern pattern = Pattern.compile("(.+):(\\d+)");
				Matcher matcher = pattern.matcher(serverAddress);
				Socket socket = null;
				
				try
				{
					if (matcher.matches())
						socket = new Socket(matcher.group(1), Integer.parseInt(matcher.group(2)));
				}
				catch (NumberFormatException e) {}
				catch (UnknownHostException e)
				{
					e.printStackTrace();
				}
				catch (IOException e) {}

				if (socket != null)
				{
					Connection connection = this.addConnection(socket);
					
					if (connection != null)
					{
						connection.setIsServerConnection(true);
						this.connections.putServerConnection(connection.connectionId, connection);
						sendAddServerMessage(connection);
					}
				}
			}
		}
	}
	private void sendAddServerMessage(Connection connection)
	{
		AddServerRequestMessage message = new AddServerRequestMessage();
		
		try
		{
			InetAddress address = InetAddress.getLocalHost();
			String ip = address.getHostAddress();
			
			message.setIpAddress(ip + ":" + config.getPort());
		}
		catch (UnknownHostException e) { e.printStackTrace(); }
		
		if (message.getIpAddress() != null)
		{
			connection.sendMessage(message);
		}
	}
	private Connection addConnection(Socket socket)
	{
		Connection connection = null;
		
		try
		{
			connection = new Connection(getConnectionId(), socket);
		}
		catch (InstantiationException e)
		{
			e.printStackTrace();
		}
		
		if (connection != null && connection.connectionId >= 0)
		{
			connections.put(connection.connectionId, connection);
		}
		
		return connection;
	}
	public void uncaughtThrowableThrown(Throwable t)
	{
		System.err.print("Uncaught exception found: "); t.printStackTrace();
		this.restart();
	}
	public void saveConfig()
	{
		configManager.saveConfiguration(config);
	}
	public void restart()
	{
		stop();
		start();
	}
	public boolean isReady()
	{
		return ready;
	}
	public void run()
	{
		try
		{
			start();
			
			if (serverSocket != null)
			{
				while (running)
				{
					try
					{
						Socket socket = serverSocket.accept();
						
						this.addConnection(socket);
					}
					catch (IOException e) {}
				}	
			}
		}
		catch (Throwable t) { this.uncaughtThrowableThrown(t); }
	}
	public static void main(String[] args)
	{
		Server server = null;
		
		try
		{
			server = new Server();
		}
		catch (InstantiationException e)
		{
			System.err.println("Failed to start server");		
		}
		
		server.saveConfig();
		
		if (server != null)
			server.run();
	}
}
